﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Security;
using DanubiusInfo.Orchard.Ldap.Events;
using DanubiusInfo.Orchard.Ldap.Models;
using Orchard;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.Security;
using Orchard.Users.Models;
using Orchard.Users.Services;

namespace DanubiusInfo.Orchard.Ldap.Services
{
    public class LdapMembershipService : IMembershipService
    {
        readonly IOrchardServices orchardServices;
        readonly IEncryptionService encryptionService;
        readonly ILdapServiceFactory ldapServiceFactory;
        readonly ILdapDirectoryCache ldapDirectoryCache;
        readonly ILdapUserEventHandler ldapUserEventHandler;
        readonly Lazy<IUserService> userService;
        readonly Lazy<IMembershipService> originalMembershipService;

        public LdapMembershipService(
            IOrchardServices orchardServices,
            IEncryptionService encryptionService,
            ILdapServiceFactory ldapServiceFactory,
            ILdapDirectoryCache ldapDirectoryCache,
            ILdapUserEventHandler ldapUserEventHandler,
            Lazy<IUserService> userService)
        {
            this.orchardServices = orchardServices;
            this.encryptionService = encryptionService;
            this.ldapServiceFactory = ldapServiceFactory;
            this.ldapDirectoryCache = ldapDirectoryCache;
            this.ldapUserEventHandler = ldapUserEventHandler;
            this.userService = userService;
            originalMembershipService = new Lazy<IMembershipService>(() =>
                this.orchardServices.WorkContext.Resolve<IEnumerable<IMembershipService>>().Single(x => !(x is LdapMembershipService)));

            Logger = NullLogger.Instance;
            T = NullLocalizer.Instance;
        }

        public ILogger Logger { get; set; }
        public Localizer T { get; set; }

        public MembershipSettings GetSettings()
        {
            var settings = new MembershipSettings();
            // accepting defaults
            return settings;
        }

        public IUser CreateUser(CreateUserParams createUserParams)
        {
            return originalMembershipService.Value.CreateUser(createUserParams);
        }

        public IUser GetUser(string username)
        {
            return originalMembershipService.Value.GetUser(username);
        }

        // MIGRATION copied from Orchard.Users.Services.MembershipService
        public IUser ValidateUser(string userNameOrEmail, string password)
        {
            var user = FetchUser(userNameOrEmail);

            if (user == null)
                return CreateUserOnTheFly(userNameOrEmail, password);

            if (user.RegistrationStatus != UserStatus.Approved ||
                user.EmailStatus != UserStatus.Approved)
                return null;

            var userLdap = user.As<UserLdapPart>();
            if (userLdap.LdapDirectoryId != null)
            {
                // authenticating with LDAP directory
                var directory = ldapDirectoryCache.GetDirectory(userLdap.LdapDirectoryId.Value);
                if (directory == null ||
                    !AuthenticateWith(ldapServiceFactory.For(directory), user.UserName, password))
                    return null;
            }
            else
            {
                // authenticating with the original method
                if (!ValidatePassword(user.As<UserPart>().Record, password))
                    return null;
            }

            return user;
        }

        UserPart FetchUser(string userName, bool acceptEmailToo = true)
        {
            var lowerName = userName == null ? "" : userName.ToLowerInvariant();

            var user = orchardServices.ContentManager.Query<UserPart, UserPartRecord>().Where(u => u.NormalizedUserName == lowerName).List().FirstOrDefault();

            if (acceptEmailToo && user == null)
                user = orchardServices.ContentManager.Query<UserPart, UserPartRecord>().Where(u => u.Email == lowerName).List().FirstOrDefault();
            
            return user;
        }

        bool AuthenticateWith(ILdapService ldapService, string userName, string password)
        {
            try
            {
                return ldapService.Authenticate(userName, password);
            }
            catch (Exception ex)
            {
                Logger.Error("An error occurred during authentication. Details: {0}", ex);
                return false;
            }
        }

        UserPart CreateUserOnTheFly(string userNameOrEmail, string password)
        {
            // trying to find an ldap directory which can authenticate the user
            var directories = ldapDirectoryCache.GetDirectories();
            ILdapService ldapService = null;
            foreach (var directoryIntf in directories)
                if (directoryIntf.EnableOnTheFlyUserCreation)
                {
                    var ldapServiceCandidate = ldapServiceFactory.For(directoryIntf);
                    if (AuthenticateWith(ldapServiceCandidate, userNameOrEmail, password))
                    {
                        ldapService = ldapServiceCandidate;
                        break;
                    }
                }

            if (ldapService == null)
                return null;

            if (string.Equals(ldapService.Directory.ServiceAccountUserName, userNameOrEmail, StringComparison.InvariantCultureIgnoreCase))
            {
                Logger.Warning("On-the-fly creation of user {0} failed. {1}", userNameOrEmail, "Cannot create user for directory service user.");
                return null;
            }

            // on-the-fly user creation
            CreateUserParams createUserParams;
            try
            {
                createUserParams = ldapService.GetCreateUserParams(userNameOrEmail, password);
            }
            catch (Exception ex)
            {
                if (ex is ApplicationException || ex is System.DirectoryServices.Protocols.DirectoryException)
                {
                    Logger.Error("An error occurred during on-the-fly creation of user {0}. Details: {1}", userNameOrEmail, ex);
                    return null;
                }
                else
                    throw;
            }

            if (!userService.Value.VerifyUserUnicity(createUserParams.Username, createUserParams.Email))
            {
                Logger.Warning("On-the-fly creation of user {0} failed. {1}", userNameOrEmail, string.Format("E-mail {0} is already in use.", createUserParams.Email));
                return null;
            }

            var user = originalMembershipService.Value.CreateUser(createUserParams);
            if (user != null)
            {
                user.As<UserLdapPart>().LdapDirectoryId = ldapService.Directory.Id;

                var context = new UserCreatedOnTheFlyContext(user, createUserParams);
                ldapUserEventHandler.CreatedOnTheFly(context);
                if (context.Cancel)
                    user = null;
            }
               
            if (user == null)
            {
                Logger.Warning("On-the-fly creation of user {0} failed. {1}", userNameOrEmail, "Operation was cancelled.");
                return null;
            }

            return user.As<UserPart>();
        }

        public void SetPassword(IUser user, string password)
        {
            var userLdap = user.As<UserLdapPart>();
            if (userLdap.LdapDirectoryId == null &&
                ValidateUser(user.UserName, userLdap.CurrentPassword) == null)
                throw new ApplicationException("Invalid username or password.");

            originalMembershipService.Value.SetPassword(user, password);

            if (userLdap.LdapDirectoryId != null)
            {
                var directory = ldapDirectoryCache.GetDirectory(userLdap.LdapDirectoryId.Value);
                var ldapService = ldapServiceFactory.For(directory);

                bool passwordChanged; 
                try
                {
                    passwordChanged = ldapService.ChangePassword(user.UserName, userLdap.CurrentPassword, password);
                }
                catch (Exception ex)
                {
                    if (ex is ApplicationException || ex is System.DirectoryServices.Protocols.DirectoryException)
                        Logger.Error("An error occurred when changing password of user {0}. Details: {1}", user.UserName, ex);
                    throw new ApplicationException("Password could not be changed.", ex);
                }

                if (!passwordChanged)
                    throw new ApplicationException("Password could not be changed.");
            }
        }

        // MIGRATION copied from Orchard.Users.Services.MembershipService
        bool ValidatePassword(UserPartRecord partRecord, string password)
        {
            // Note - the password format stored with the record is used
            // otherwise changing the password format on the site would invalidate
            // all logins
            switch (partRecord.PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    return ValidatePasswordClear(partRecord, password);
                case MembershipPasswordFormat.Hashed:
                    return ValidatePasswordHashed(partRecord, password);
                case MembershipPasswordFormat.Encrypted:
                    return ValidatePasswordEncrypted(partRecord, password);
                default:
                    throw new ApplicationException("Unexpected password format value");
            }
        }

        // MIGRATION copied from Orchard.Users.Services.MembershipService
        private static bool ValidatePasswordClear(UserPartRecord partRecord, string password)
        {
            return partRecord.Password == password;
        }

        // MIGRATION copied from Orchard.Users.Services.MembershipService
        private static bool ValidatePasswordHashed(UserPartRecord partRecord, string password)
        {
            var saltBytes = Convert.FromBase64String(partRecord.PasswordSalt);

            var passwordBytes = Encoding.Unicode.GetBytes(password);

            var combinedBytes = saltBytes.Concat(passwordBytes).ToArray();

            byte[] hashBytes;
            using (var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm))
            {
                hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
            }

            return partRecord.Password == Convert.ToBase64String(hashBytes);
        }

        // MIGRATION copied from Orchard.Users.Services.MembershipService
        private bool ValidatePasswordEncrypted(UserPartRecord partRecord, string password)
        {
            return String.Equals(password, Encoding.UTF8.GetString(
                encryptionService.Decode(Convert.FromBase64String(partRecord.Password))), StringComparison.Ordinal);
        }
    }
}